#pragma once
#include <vector>

#include <CL\cl.h>

class OpenCL {
public:
	int devType;

	//DIAG INFO
	cl_bool device_availble;
	cl_bool device_compiler_ok;
	cl_ulong globalCacheSize;
	cl_uint globalCacheLineSize;
	cl_ulong globalMemSize;
	cl_ulong localMemSize;
	cl_uint clockSpeed;
	cl_uint computeUnits;
	cl_ulong maxMemAlloc;
	size_t maxWorkGroupSize;
	cl_uint maxItemDim;
	std::string device_version;
	std::string driver_version;
	std::string device_vendor;
	std::string device_vendor_id;
	std::string device_name;

	std::string functionName;

	cl_int err;                // error code returned from api calls

	size_t global;             // global domain size for our calculation
	size_t local;              // local domain size for our calculation

	cl_platform_id cpPlatform; // OpenCL platform
	cl_device_id device_id;    // compute device id
	cl_context context;        // compute context
	cl_command_queue commands; // compute command queue
	cl_program program;        // compute program
	cl_kernel kernel;          // compute kernel

	std::vector<cl_mem> arg;
	std::vector<std::string> args;
	
	std::string getPath();

	void getIds();

	void getNames();

	void createProgram(const char *filePath, std::string name);

public:
	OpenCL(const char *source, std::string start, int type = CL_DEVICE_TYPE_CPU);

	~OpenCL();

	template <typename T> cl_mem setArgument(int id, size_t size, T data, cl_mem_flags flags) {
		cl_mem input = clCreateBuffer(this->context, flags,  size, NULL, NULL);
		if (!input) {
			throw std::string("Error: Failed to allocate device memory!");
		}

		// Transfer the input vector into device memory
		this->err = clEnqueueWriteBuffer(this->commands, input, CL_TRUE, 0, size, data, 0, NULL, NULL);

		if (this->err != CL_SUCCESS) {
			throw string("Error: Failed to write to source array!");
		}
		
		// Set the arguments to the compute kernel
		this->err = 0;
		this->err  = clSetKernelArg(this->kernel, id, sizeof(cl_mem), &input);
		
		if (err != CL_SUCCESS) {
			throw string("Error: Failed to set kernel arguments! ");
		}
		return input;
	}

	template <typename T> std::string ArrayToString(T data, size_t size) {
		int maxSize = 16;
		
		stringstream stream;
		stream << "[ ";
		int amount = (int)size / (int)sizeof(T);
		int org = amount;
		bool limited = false;
		if ( amount > maxSize ) { 
			amount = maxSize;
			limited = true;
			stream << "size=" << org << " : ";
			for ( int i = 0; i < amount - (maxSize / 2); i++ ) 
			{ 		
				stream << data[i];
				if ( i != amount - (maxSize / 2) - 1) { stream << ", ";  }
			}
			stream << " ...  ";
			for ( int i = amount - (maxSize / 2); i < amount; i++ ) 
			{ 		
				stream << data[i];
				if ( i != amount - 1) { stream << ", ";  }
			}
		}
		//Print ALL
		else {
			for ( int i = 0; i < amount; i++ ) 
			{ 		
				stream << data[i];
				if ( i != amount - 1) { stream << ", ";  }
			}
		}

		stream << " ]";
		return stream.str();
	}
	

	template <typename T> void setArgumentReadOnly(int id, size_t size, T data) {
		this->arg.push_back(this->setArgument<T>(id, size, data, CL_MEM_ALLOC_HOST_PTR | CL_MEM_READ_ONLY));
		this->args.push_back( ArrayToString<T>(data, size) );
	}

	template <typename T> void setArgumentWriteOnly(int id, size_t size, T data) {
		this->arg.push_back(this->setArgument<T>(id, size, data, CL_MEM_ALLOC_HOST_PTR | CL_MEM_WRITE_ONLY));		
		this->args.push_back( ArrayToString<T>(data, size) );
	}

	template <typename T> void setArgumentReadWrite(int id, size_t size, T data) {
		this->arg.push_back(this->setArgument<T>(id, size, data,  CL_MEM_ALLOC_HOST_PTR | CL_MEM_USE_HOST_PTR ));
		this->args.push_back(ArrayToString<T>(data, size) );
	}

	template <typename T> void readArgument(int id, size_t size, T data) {
		cl_mem output = this->arg[id];
		// Read back the results from the device to verify the output
		this->err = clEnqueueReadBuffer(this->commands, output, CL_TRUE, 0, size, data, 0, NULL, NULL );
		this->checkError(this->err);
		this->args.push_back( ArrayToString<T>(data, size) );
	}

	void execute(int count);

	std::string checkError(int err);

	std::string ToString();
};